/*
* Copyright 2010 Pavel Kozhin 
*
* Licensed under the Apache License, Version 2.0 (the "License"); 
* you may not use this file except in compliance with the License. 
* You may obtain a copy of the License at 
* 
* http://www.apache.org/licenses/LICENSE-2.0
* 
* Unless required by applicable law or agreed to in writing, software 
* distributed  under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
* See the License for the specific language governing permissions and 
* limitations under the License.
*
* Author: Pavel Kozhin.
*/

package org.etherframework.wire
{
	import flash.events.Event;
	import flash.utils.Dictionary;
	import flash.utils.getQualifiedClassName;
	
	import org.etherframework.behaviour.rules.core.Control;
	import org.etherframework.core.Access;
	import org.etherframework.core.DomainAccess;
	import org.etherframework.core.IBridge;
	import org.etherframework.core.IControllerPart;
	import org.etherframework.core.ether_internal;
	import org.etherframework.core.events.DataEvent;
	import org.etherframework.core.util.DataAccess;
	import org.etherframework.metadata.MetadataInfo;
	import org.etherframework.metadata.Metatag;
	import org.etherframework.metadata.MetatagInfo;
	import org.etherframework.reflection.MetadataMember;
	import org.etherframework.wire.wirings.IInjection;
	import org.etherframework.wire.wirings.IWiring;
	import org.etherframework.wire.wirings.Proxy;
	
	use namespace ether_internal;
	
	[ExcludeClass]
	public class WireController implements IControllerPart
	{
		private var wiring:Array = [];
		
		private var packages:Array = [];
		
		private var injections:Array = [];
		
		private var injectionMap:Dictionary;
		
		private var storage:WireStorage;
		
		private var isInitialized:Boolean = false;
		
		private var shared:DataAccess;
		
		private var token:String;
		
		private var hooked:Array = [];
		
		private var resolvers:Array = [];
		
		private var data:WiringData;
		
		public function WireController(tags:Array)
		{
			_tags = tags;
			storage = new WireStorage(new Dictionary(true));
			parseTags(tags);
		}
		
		public function initialize():void
		{
			if (isInitialized && !_bridge)
				return;
			
			shared = bridge.shared;
			wireHooks(hooked);
			injectStatic(injectionMap);
			addItem(bridge.host);
			
			bridge.host.addEventListener(Event.REMOVED_FROM_STAGE, viewRemovedHandler, true, 200);
			bridge.host.addEventListener(Event.ADDED, viewAddedHandler, false, 200);
			
			isInitialized = true;
		}
		
		public function uninitialize():void
		{
			if (!isInitialized)
				return;
			
			var c:Dictionary = storage.cache;
			for each(var info:WireInfo in c)
			{
				unwireByInfo(info);
			}
			
			isInitialized = false;
		}
		
		private var _tags:Array = [];
		public function get tags():Array
		{
			return _tags;
		}
		
		public function addItem(value:Object):Object
		{
			var qName:String = getQualifiedClassName(value);
			
			if(!isWiringAllowed(value, qName))
				return null;
			
			var result:WireInfo = bridge.getWire(value);
			
			if (result)
				return result;
			
			var metaInfo:MetadataInfo = bridge.getMetadata(qName);
			
			if (!metaInfo)
				metaInfo = bridge.addMetadata(value);
			
			if (!metaInfo)
				throw new Error("Ether: Can't extract metadata.");
			
			if (metaInfo.metatags.length)
				result = wireMetadata(value, metaInfo);
			
			if (injectionMap[qName])
				result = wireInjection(value, injectionMap[qName], result);
			
			if (result)
			{
				storage.addItem(result);
				bridge.shared.addItem(value, token);
				bridge.interaction.dispatchItemWired(value, Access.LOCAL, DomainAccess.SINGLE);
			}
			
			return result;
		}
		
		public function removeItem(value:Object):Object
		{
			var info:WireInfo = storage.getItem(value);
			
			if (info)
			{
				unwireByInfo(info);
			}
			
			bridge.shared.removeItem(value);
			
			// TODO: Allow entity to know that it was unwired.
			
			return info;
		}
		
		public function getItem(value:*):*
		{
			return storage.getItem(value);
		}
		
		private var _bridge:IBridge;
		
		public function get bridge():IBridge
		{
			return _bridge;
		}
		
		public function set bridge(value:IBridge):void
		{
			_bridge = value;
			
			if (value)
			{
				token = value.token;
			}
		}
		
		private function parseTags(tags:Array):void
		{
			var tag:Wire;
			var tagsLen:int = tags.length;
			
			for (var i:int = 0; i < tagsLen; ++i)
			{
				tag = tags[i];
				
				if (tag.packages.length)
					packages = packages.concat(tag.packages);
				
				if (tag.injections.length)
					injections = injections.concat(tag.injections);
				
				if (tag.hooked.length)
					hooked = hooked.concat(tag.hooked);
				
				if (tag.resolvers.length)
					resolvers = resolvers.concat(tag.resolvers);
			}
			injectionMap = createInjectionMap(injections);
		}
		
		private function wireMetadata(obj:Object, metaInfo:MetadataInfo):WireInfo
		{
			var metatags:Array = metaInfo.metatags;
			var metatagsLen:int = metatags.length;
			var metatagName:String;
			var wireItems:Array;
			var wireItem:Object;
			var wireItemsLen:int;
			var metatag:MetatagInfo;
			var wiring:IWiring;
			var wiringClass:Class;
			var wirings:Dictionary;
			var isClass:Boolean = (obj is Class);
			var j:int;
			
			if (!data)
				data = new WiringData(_bridge, resolvers);
			
			for (var i:int = 0; i < metatagsLen; ++i)
			{
				metatag = metatags[i];
				metatagName = metatag.name;
				wiringClass = metatag.generator;
				
				if (!wiringClass)
					continue;
				
				wireItems = metatag.items;
				wireItemsLen = wireItems.length;
				
				for (j = 0; j < wireItemsLen; ++j)
				{
					wireItem = wireItems[j];
					
					if ((wireItem is MetadataMember) && (wireItem.isStatic != isClass))
						continue;
					
					wiring = new wiringClass();
					
					if(wiring.wire(metatagName, obj, wireItem, data))
					{
						if (!wirings)
							wirings = new Dictionary();
						
						if (!wirings[metatagName])
							wirings[metatagName] = [];
						
						wirings[metatagName].push(wiring);
					}
				}
			}
			
			if (wirings)
				return new WireInfo(obj, wirings);
			
			return null;
		}
		
		private function wireInjection(obj:Object, injections:Array, wireInfo:WireInfo):WireInfo
		{
			var injection:Injection;
			var items:Array;
			var itemsLen:int;
			var item:IInjection;
			var itemName:String;
			var wirings:Dictionary;
			var j:int;
			
			if (!data)
				data = new WiringData(_bridge, resolvers);
			
			for (var i:int = 0; i < injections.length; ++i)
			{
				injection = injections[i];
				items = injection.items;
				itemsLen = items.length;
				
				for (j = 0; j < itemsLen; ++j)
				{
					item = items[j] as IInjection;
					
					if (!item)
						continue;
					
					itemName = getQualifiedClassName(item).split("::")[1];
					
					if (item.inject(itemName, obj, data))
					{
						if(!wirings)
							wirings = (wireInfo) ? wireInfo.wirings : new Dictionary();
						
						if (!wirings[itemName])
							wirings[itemName] = [];
						
						wirings[itemName].push(item);
					}
				}
			}
			
			if (wireInfo)
			{
				return wireInfo;
			}
			else if (wirings)
			{
				return new WireInfo(obj, wirings);
			}
			
			return null;
		}
		
		private function injectStatic(injectionMap:Dictionary):void
		{
			var i:int;
			var j:int;
			var injection:Injection;
			var injections:Array;
			var injectionsLen:int;
			var injectionItems:Array;
			var injectionItem:IInjection;
			var injectionItemsLen:int;
			var itemName:String;
			var clazz:Object;
			
			if (!data)
				data = new WiringData(_bridge, resolvers);
			
			for (var className:String in injectionMap)
			{
				injections = injectionMap[className];
				injectionsLen = injections.length;
				
				for (i = 0; i < injectionsLen; ++i)
				{
					injection = injections[i];
					clazz = (injection.target is Class) ? injection.target : bridge.util.getClass(className);
					
					if (!clazz)
						continue;
					
					injectionItems = injection.items.concat();
					
					for (j = 0; j < injectionItems.length; ++j)
					{
						if ((injectionItems[j] is Proxy) && (injectionItems[j].statical))
						{
							IInjection(injectionItems[j]).inject(Metatag.PROXY, clazz, data);
							injectionItems.splice(j, 1);
						}
					}
					
					injectionItemsLen = injectionItems.length;
					
					for (j = 0; j < injectionItemsLen; ++j)
					{
						injectionItem = injectionItems[j] as IInjection;
						if (injectionItem)
						{
							itemName = getQualifiedClassName(injectionItem).split("::")[1];
							injectionItem.inject(itemName, clazz, data);
						}
					}
				}
			}
		}
		
		private function createInjectionMap(injections:Array):Dictionary
		{
			var result:Dictionary = new Dictionary();
			var injectionsLen:int = injections.length;
			var injection:Injection;
			var className:String;
			var j:int = 0;
			
			for (var i:int = 0; i < injectionsLen; ++i)
			{
				injection = injections[i];
				
				if (!injection.target)
					continue;
				
				className = (injection.target is Class) ? 
					getQualifiedClassName(injection.target) : 
					injection.target as String;  
				
				if (!className)
					continue;
				
				if (!result[className])
					result[className] = [];
				
				result[className].push(injection);
			}
			
			return result;
		}
		
		private function wireHooks(hooks:Array):void
		{
			var max:int = hooks.length;
			
			for (var i:int = 0; i < max; ++i)
			{
				if (hooks[i] is Class)
				{
					addItem(hooks[i]);
				}
			}
		}
		
		private function isWiringAllowed(obj:Object, className:String):Boolean
		{
			if (obj is Control)
				return true;
			
			if ((className.indexOf("spark.") == 0) || 
				(className.indexOf("mx.") == 0) || 
				(className.indexOf("fl.") == 0) ||
				(className.indexOf("flash.") == 0) ||
				(className.indexOf("org.osmf.") == 0))
			{
				return false;
			}
			
			if (packages.length)
			{
				var max:int = packages.length;
				for (var i:int = 0; i < max; ++i)
				{
					if (className.indexOf(packages[i]) >= 0)
					{
						return true;
					}
				}
				
				return false;
			}
			
			return true;
		}
		
		private function unwireByInfo(info:WireInfo):void
		{
			var wiringsDict:Dictionary;
			var wiringsArr:Array;
			var wiringsArrLen:int;
			var i:int;
			
			wiringsDict = info.wirings;
			for each(wiringsArr in wiringsDict)
			{
				wiringsArrLen = wiringsArr.length;
				for (i = 0; i < wiringsArrLen; ++i)
				{
					wiringsArr[i].unwire(info.target);
				}
			}
		}
		
		private function viewAddedHandler(event:Event):void
		{
			if (!shared.getItem(event.target))
			{
				addItem(event.target);
			}
		}
		
		private function viewRemovedHandler(event:Event):void
		{
			if (shared.getItem(event.target) == token)
			{
				removeItem(event.target);
			}
		}
		
	}
}